package com.langbricks.nlp.prs.printer;

import gate.creole.AbstractLanguageAnalyser;
import gate.creole.ResourceInstantiationException;
import gate.creole.ExecutionException;

import gate.*;
import gate.Document;
import gate.util.OffsetComparator;
import gate.util.Out;
import gate.util.Strings;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.util.*;
import java.io.*;
import java.net.URL;
import java.net.URISyntaxException;

import org.w3c.dom.Element;
import com.sun.org.apache.xml.internal.serialize.XMLSerializer;
import com.sun.org.apache.xml.internal.serialize.OutputFormat;


/*
 *  FeaturesPrinterPR.java
 *
 *  Copyright (c) 2010, The Langbricks.com . See the file COPYRIGHT.txt in the software.
 *
 *  This file is a self contianed Language Processing Resource for GATE (see http://gate.ac.uk/), and is free
 *  software, licenced under the GNU Library General Public License,
 *  Version 2, June 2010 (in the distribution as file licence.html,
 *  and also available at http://www.gnu.org/licenses/old-licenses/gpl-2.0.html).
 *
 *  Andrei Shingel, 22/June/2010
 *  Email: info@langbricks.com
 *
 */
public class FeaturesPrinterPR extends AbstractLanguageAnalyser implements ProcessingResource {

    /**
     * Input AnnotationSet name
     */
    private String inputAnnotationSet;
    /**
     * Name of the annotation which contains Features to print
     */
    private String annotationName = null;
    /**
     * List of Features to print
     */
    private List featuresList = null;
    /**
     * Symbol which will split features in plain text
     */
    private String featuresSeparatorSymbol;
    /**
     * Output destiantion. If no destination defined data will be printed out just to the GATE Message window
     */
    private URL outputDestination = null;

    /**
     * Output format. Can be xml or plain. By default we have plain
     */
    private String outputFormat = FeaturesPrinterPR.PLAIN_OUTPUT_FORMAT;
    /**
     * Encoding. By default UTF-8
     */
    private String encoding = "UTF-8";

    /**
     * Print content property. By default content will be printed
     */
    private boolean printContent = Boolean.TRUE;
    /**
     * All in one output or per file output
     */
    private boolean commulativeOutput = Boolean.FALSE;
    /**
     * Plain output format value
     */
    private static String PLAIN_OUTPUT_FORMAT = "plain";
    /**
     * XML output format value
     */
    private static String XML_OUTPUT_FORMAT = "xml";
    /**
     * Root Annotations node name
     */
    private static final String ROOT_ANNOTATIONS_NODE = "Annotations";
    /**
     * parameter which avoid next parameter check
     */
    private boolean propertiesSetProperly = true;

    public Resource init() throws ResourceInstantiationException {
        return super.init();
    }

    /**
     * Main PR executable method
     *
     * @throws ExecutionException
     */
    public void execute() throws ExecutionException {
        int positionDoc = getCorpus().indexOf(getDocument());
        // check parameters
        String message = checkParameters();
        if (message != null) {
            throw new ExecutionException(message);
        }
        // check if the corpus already processed
        if (positionDoc == getCorpus().size() - 1) {
            fireProgressChanged((100 * positionDoc) / corpus.size());
            if (getOutputFormat().equalsIgnoreCase(FeaturesPrinterPR.PLAIN_OUTPUT_FORMAT)) {
                processPlainOutputFormat();
            } else if (getOutputFormat().equalsIgnoreCase(FeaturesPrinterPR.XML_OUTPUT_FORMAT)) {
                processXMLOutputFormat();
            } else {
                throw new ExecutionException("Unknown Output format. Please define a xml or plain ");
            }
            fireProcessFinished();
        }
    }

    /**
     * Method build empty xml document
     *
     * @return Empty XML document
     */
    private org.w3c.dom.Document getEmptyXMLDocument() {
        org.w3c.dom.Document document = null;
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        try {
            DocumentBuilder documentBuilder = factory.newDocumentBuilder();
            document = documentBuilder.newDocument();
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        }
        return document;
    }

    /**
     * Method process output when XML format selected for output
     */
    private void processXMLOutputFormat() {
        // create empty xml document
        org.w3c.dom.Document xmlDocument = getEmptyXMLDocument();
        Element root = xmlDocument.createElement(FeaturesPrinterPR.ROOT_ANNOTATIONS_NODE);
        xmlDocument.appendChild(root);
        // we reached the end of corpus processing.Process all Documents from corpus
        Iterator<Document> documentIterator = getCorpus().iterator();
        boolean deleteDoc = (corpus.getDataStore() != null);
        while (documentIterator.hasNext()) {
            Document currentDocument = documentIterator.next();
            AnnotationSet inputAS = inputAnnotationSet == null || inputAnnotationSet.trim().length() == 0 ? currentDocument.getAnnotations() : currentDocument.getAnnotations(inputAnnotationSet);
            if (inputAS != null && inputAS.size() > 0) {
                AnnotationSet annotationsToPrintSet = inputAS.get(getAnnotationName());
                if (getFeaturesList() != null && getFeaturesList().size() > 0) {
                    List<Annotation> sortedAnnotations = new ArrayList(annotationsToPrintSet);
                    Collections.sort(sortedAnnotations, new OffsetComparator());
                    Iterator<Annotation> iterator = sortedAnnotations.iterator();
                    while (iterator.hasNext()) {
                        Annotation annotation = iterator.next();
                        String annotationName = annotation.getType();
                        Iterator featuresIterator = getFeaturesList().iterator();
                        Map attributesMap = new HashMap();
                        while (featuresIterator.hasNext()) {
                            Object feature = featuresIterator.next();
                            Object featureValue = annotation.getFeatures().get(feature);
                            attributesMap.put(feature, featureValue);
                        }
                        Element newNode = createtNode(xmlDocument, attributesMap, annotationName);
                        if (getPrintContent()) {
                            String content = currentDocument.getContent().toString().substring(annotation.getStartNode().getOffset().intValue(), annotation.getEndNode().getOffset().intValue());
                            newNode.setTextContent(content);
                        }
                        root.appendChild(newNode);
                    }
                } else if (getPrintContent()) {
                    // well somebody want to printout only content of annoations...
                    List<Annotation> sortedAnnotations = new ArrayList(annotationsToPrintSet);
                    Collections.sort(sortedAnnotations, new OffsetComparator());
                    Iterator<Annotation> iterator = sortedAnnotations.iterator();
                    while (iterator.hasNext()) {
                        Annotation annotation = iterator.next();
                        String annotationName = annotation.getType();
                        Map attributesMap = new HashMap();
                        Element newNode = createtNode(xmlDocument, attributesMap, annotationName);
                        String content = currentDocument.getContent().toString().substring(annotation.getStartNode().getOffset().intValue(), annotation.getEndNode().getOffset().intValue());
                        newNode.setTextContent(content);
                        root.appendChild(newNode);
                    }
                }
            }
            // cleanup document
            corpus.unloadDocument(currentDocument);
            if (deleteDoc) {
                Factory.deleteResource(currentDocument);
            }
        }
        if (getOutputDestination() == null) {
            // just print out xml to the GATE Messages window
            Out.println(toString(xmlDocument));
        } else {
            writeXMLOutput(xmlDocument);
        }
    }

    /**
     * Converts an Element Node or Document into its text representation
     * <p/>
     * Converts either the SubTree designated by an Element node or an
     * entire tree, specified by a Document object into an XML Text
     * representation.
     *
     * @param node The DOM node (doc or element) to convert
     * @return The input string's representation
     */
    private String toString(org.w3c.dom.Node node) {
        StringWriter writer = new StringWriter();
        XMLSerializer serial = new XMLSerializer();
        serial.setOutputCharStream(writer);
        OutputFormat format = new OutputFormat((org.w3c.dom.Document) node, "UTF-8", false);
        format.setPreserveSpace(true);
        serial.setOutputFormat(format);
        if (node.getNodeType() == org.w3c.dom.Node.DOCUMENT_NODE) {
            try {
                serial.serialize((org.w3c.dom.Document) node);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            try {
                serial.serialize((Element) node);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return writer.toString();
    }

    /**
     * Method write xml document to the defined destination
     *
     * @param xmlDocument
     */
    private void writeXMLOutput(org.w3c.dom.Document xmlDocument) {
        //final StreamResult streamResult = new StreamResult(new File(getOutputDestination().getPath()));
        String filePath = getOutputDestination().getPath();
        File outputFile = new File(filePath);
        if (outputFile.isDirectory()) {
            // well, someone defined folder intead of File. Let use default output file name
            char lastCharacter = filePath.charAt(filePath.length() - 1);
            if (!(lastCharacter == '\\' || lastCharacter == '/')) {
                // let correct the name
                outputFile = new File(filePath + File.separator + "FEOutput.xml");
            } else {
                outputFile = new File(filePath + "FEOutput.xml");
            }
        }
        final StreamResult streamResult = new StreamResult(outputFile);
        final TransformerFactory tf = TransformerFactory.newInstance();
        try {
            final Transformer serializer = tf.newTransformer();
            serializer.setOutputProperty(OutputKeys.METHOD, "xml");
            serializer.setOutputProperty(OutputKeys.VERSION, "1.0");
            serializer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
            serializer.setOutputProperty(OutputKeys.INDENT, "yes");
            serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
            serializer.transform(new DOMSource(xmlDocument), streamResult);
            Out.println("Data saved to : " + outputFile.getAbsolutePath());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Method process output when plain format selected for output
     */
    private void processPlainOutputFormat() {
        // we reached the end of corpus processing.Process all Documents from corpus
        Iterator<Document> documentIterator = getCorpus().iterator();
        // method which create a header string
        String header = getHeaderString(getFeaturesList());
        List outputList = new ArrayList();
        outputList.add(header);
        boolean deleteDoc = (corpus.getDataStore() != null);
        while (documentIterator.hasNext()) {
            Document currentDocument = documentIterator.next();
            AnnotationSet inputAS = inputAnnotationSet == null || inputAnnotationSet.trim().length() == 0 ? currentDocument.getAnnotations() : currentDocument.getAnnotations(inputAnnotationSet);
            if (inputAS != null && inputAS.size() > 0) {
                AnnotationSet annotationsToPrintSet = inputAS.get(getAnnotationName());
                if (getFeaturesList() != null && getFeaturesList().size() > 0) {
                    List<Annotation> sortedAnnotations = new ArrayList(annotationsToPrintSet);
                    Collections.sort(sortedAnnotations, new OffsetComparator());
                    Iterator<Annotation> iterator = sortedAnnotations.iterator();
                    // just print out values directly to the GATE message window
                    while (iterator.hasNext()) {
                        Annotation annotation = iterator.next();
                        Iterator featuresIterator = getFeaturesList().iterator();
                        // local quick output
                        StringBuffer outputBuffer = new StringBuffer();
                        if (getPrintContent()) {
                            String content = currentDocument.getContent().toString().substring(annotation.getStartNode().getOffset().intValue(), annotation.getEndNode().getOffset().intValue());
                            outputBuffer.append(content).append(getFeaturesSeparatorSymbol());
                        }
                        while (featuresIterator.hasNext()) {
                            Object feature = featuresIterator.next();
                            outputBuffer.append(annotation.getFeatures().get(feature));
                            if (featuresIterator.hasNext()) {
                                outputBuffer.append(getFeaturesSeparatorSymbol());
                            }
                        }
                        outputList.add(outputBuffer.toString());
                    }
                } else if (getPrintContent()) {
                    // just print content of the Annotation
                    List<Annotation> sortedAnnotations = new ArrayList(annotationsToPrintSet);
                    Collections.sort(sortedAnnotations, new OffsetComparator());
                    Iterator<Annotation> iterator = sortedAnnotations.iterator();
                    // just print out values directly to the GATE message window
                    while (iterator.hasNext()) {
                        Annotation annotation = iterator.next();
                        StringBuffer outputBuffer = new StringBuffer();
                        if (getPrintContent()) {
                            String content = currentDocument.getContent().toString().substring(annotation.getStartNode().getOffset().intValue(), annotation.getEndNode().getOffset().intValue());
                            outputBuffer.append(content);
                        }
                        outputList.add(outputBuffer.toString());
                    }
                }
            }
            // cleanup document
            corpus.unloadDocument(currentDocument);
            if (deleteDoc) {
                Factory.deleteResource(currentDocument);
            }
        }
        // check output destination
        if (getOutputDestination() == null) {
            // just print out results
            Iterator iterator = outputList.iterator();
            while (iterator.hasNext()) {
                Out.println(iterator.next());
            }
        } else {
            write(outputList, getOutputDestination().getPath());
        }
    }

    /**
     * The method create the node with defined parameters
     *
     * @param document
     * @param attributes
     * @param nodeName
     * @return
     */
    protected synchronized Element createtNode(org.w3c.dom.Document document, Map attributes, String nodeName) {
        Element element = document.createElement(nodeName);
        if (attributes != null) {
            Iterator iterator = attributes.keySet().iterator();
            while (iterator.hasNext()) {
                String key = String.valueOf(iterator.next());
                element.setAttribute(key, String.valueOf(attributes.get(key)));
            }
        }
        return element;
    }

    /**
     * Method create a header for the page.Based on the feature name
     *
     * @param featuresList List with features names.
     * @return String where features presented as a sequence of their names
     */
    private String getHeaderString(List featuresList) {
        Iterator iterator = featuresList.iterator();
        StringBuffer stringBuffer = new StringBuffer();
        if (getPrintContent()) {
            // put here predifined header for header
            stringBuffer.append("Annotation Content").append(getFeaturesSeparatorSymbol());
        }
        while (iterator.hasNext()) {
            Object feature = iterator.next();
            stringBuffer.append(feature);
            if (iterator.hasNext()) {
                stringBuffer.append(getFeaturesSeparatorSymbol());
            }
        }
        return stringBuffer.toString();
    }

    /**
     * Method will write lines of texts to the path
     *
     * @param text
     * @param filePath
     */
    private void write(List text, String filePath) {
        try {
            File outputFile = new File(filePath);
            if (outputFile.isDirectory()) {
                // well, someone defined folder intead of File. Let use default output file name
                char lastCharacter = filePath.charAt(filePath.length() - 1);
                if (!(lastCharacter == '\\' || lastCharacter == '/')) {
                    // let correct the name
                    outputFile = new File(filePath + File.separator + "FEOutput.txt");
                } else {
                    outputFile = new File(filePath + "FEOutput.txt");
                }
            }
            FileWriter fstream = new FileWriter(outputFile);
            BufferedWriter out = new BufferedWriter(fstream);
            Iterator<String> iterator = text.iterator();
            String message;
            while (iterator.hasNext()) {
                message = iterator.next();
                out.write(message);
                out.newLine();
            }
            out.flush();
            out.close();
            Out.println("Data saved to : " + outputFile.getAbsolutePath());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Method check that all parameters are set properly
     *
     * @return
     */
    private String checkParameters() {
        String resultingMessage = null;
        // at least one parameter should be selected
        if ((this.featuresList == null || this.featuresList.isEmpty()) && getOutputDestination() == null && !getPrintContent()) {
            resultingMessage = ("Nothing to printed out. Please correct runtime parameters");
        }
        // Output format
        if (getOutputFormat() == null || ((!getOutputFormat().equalsIgnoreCase(FeaturesPrinterPR.PLAIN_OUTPUT_FORMAT)) && (!getOutputFormat().equalsIgnoreCase(FeaturesPrinterPR.XML_OUTPUT_FORMAT)))) {
            resultingMessage = "Unknown Output format. Please select xml or plain output format";
        }
        URL outputURL = getOutputDestination();
        File outputFile = null;
        try {
            outputFile = new File(outputURL.toURI());
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
        if (!outputFile.exists()) {
            resultingMessage = "Defined output File cannot be found.Please specify output File or Folder";
        }
        return resultingMessage;
    }

    public boolean getCommulativeOutput() {
        return commulativeOutput;
    }

    public void setCommulativeOutput(boolean commulativeOutput) {
        this.commulativeOutput = commulativeOutput;
    }

    public String getAnnotationName() {
        return annotationName;
    }

    public void setAnnotationName(String annotationName) {
        this.annotationName = annotationName;
    }

    public String getInputAnnotationSet() {
        return inputAnnotationSet;
    }

    public void setInputAnnotationSet(String inputAnnotationSet) {
        this.inputAnnotationSet = inputAnnotationSet;
    }

    public List getFeaturesList() {
        return featuresList;
    }

    public void setFeaturesList(List featuresList) {
        this.featuresList = featuresList;
    }

    public String getFeaturesSeparatorSymbol() {
        return Strings.unescape(featuresSeparatorSymbol);
    }

    public void setFeaturesSeparatorSymbol(String featuresSeparatorSymbol) {
        this.featuresSeparatorSymbol = featuresSeparatorSymbol;
    }

    public URL getOutputDestination() {
        return outputDestination;
    }

    public void setOutputDestination(URL outputDestination) {
        this.outputDestination = outputDestination;
    }

    public String getOutputFormat() {
        return outputFormat;
    }

    public void setOutputFormat(String outputFormat) {
        this.outputFormat = outputFormat;
    }

    public String getEncoding() {
        return encoding;
    }

    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    public Boolean getPrintContent() {
        return printContent;
    }

    public void setPrintContent(Boolean printContent) {
        this.printContent = printContent;
    }

    public boolean isPropertiesSetProperly() {
        return propertiesSetProperly;
    }

    public void setPropertiesSetProperly(boolean propertiesSetProperly) {
        this.propertiesSetProperly = propertiesSetProperly;
    }
}
